/*
 * BaseNetwork.java
 *
 * Created on August 11, 2007, 11:09 AM
 *
 * Copyright 2008 David D. Emory
 * 
 * This file is part of Five Points. See <http://www.fpdev.org> for
 * additional information regarding the project.
 * 
 * Five Points is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Five Points is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Five Points.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.fpdev.core.basenet;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.fpdev.core.FPEngine;
import org.fpdev.core.Scenario;
import org.fpdev.core.algo.ListenableDijkstra;
import org.fpdev.core.transit.Routes;
import org.fpdev.apps.rtemaster.gui.map.MapLayers;
import org.fpdev.util.FPUtil;
import org.jgrapht.event.TraversalListener;
import org.jgrapht.event.TraversalListenerAdapter;
import org.jgrapht.event.VertexTraversalEvent;

/**
 * The main class for the "base network," the graph and related components 
 * representing a tranportation network. Includes the graph itself (the
 * BaseNetGraph class), node/link lookup tables, the link-type-override
 * table, and other properties.
 *
 * @author demory
 */
public class BaseNetwork {

  private BaseNetGraph graph_; // the graph itself
  private Map<Integer, BNode> nodes_; // node lookup table
  private Map<Integer, BLink> links_; // link lookup table

  private int maxNodeID_,  maxLinkID_,  minDLinkID_;

  // typeOverrides_ keeps track of links whose type is overridden by child scenarios.
  // maps link ID to a lookup table (also a map) that maps scen IDs to new link types
  private Map<Integer, Map<Short, Integer>> typeOverrides_;

  /**
   * Creates a new instance of BaseNetwork
   */
  public BaseNetwork() {
    graph_ = new BaseNetGraph();
    nodes_ = new HashMap<Integer, BNode>();
    links_ = new HashMap<Integer, BLink>();
    typeOverrides_ = new HashMap<Integer, Map<Short, Integer>>();

    maxNodeID_ = maxLinkID_ = 0;
    minDLinkID_ = 0;
  }

  public List<BNode> getNodes(double x1, double y1, double x2, double y2) {
    List<BNode> toRet = new LinkedList<BNode>();


    Iterator<BNode> nodes = nodes_.values().iterator();
    while (nodes.hasNext()) {
      BNode node = nodes.next();
      if (node.getX() > x1 && node.getY() > y1 && node.getX() < x2 && node.getY() < y2) {
        toRet.add(node);
      }
    }
    return toRet;
  }

  public void updateVisibleLinks(double x1, double y1, double x2, double y2, MapLayers layers, int majorCode) {
    Iterator<BLink> links = links_.values().iterator();
    while (links.hasNext()) {
      BLink link = links.next();
      if (x2 > link.getBBX1() && x1 < link.getBBX2() && y1 < link.getBBY2() && y2 > link.getBBY1()) {
        layers.classifyLink(link, majorCode);
      }
    }
  }

  public BaseNetGraph getGraph() {
    return graph_;
  }

  public Collection<BNode> getNodes() {
    return nodes_.values();
  }

  public Collection<BLink> getLinks() {
    return links_.values();
  }

  public int nodeCount() {
    return nodes_.size();
  }

  public int linkCount() {
    return links_.size();
  }

  public void setMaxNodeID(int val) {
    maxNodeID_ = val;
  }

  public void setMaxLinkID(int val) {
    maxLinkID_ = val;
  }

  public int getMaxNodeID() {
    return maxNodeID_;
  }

  public int getMaxLinkID() {
    return maxLinkID_;
  }

  public int newDLinkID() {
    return --minDLinkID_;
  }

  public boolean linkExists(Integer id) {
    return links_.containsKey(id);
  }

  public boolean nodeExists(Integer id) {
    return nodes_.containsKey(id);
  }

  public BLink getLinkFromID(Integer id) {
    return links_.get(id);
  }

  public Set<BLink> getLinkSetFromIDSet(Set<Integer> ids) {
    Set<BLink> links = new HashSet<BLink>();
    for (Iterator<Integer> it = ids.iterator(); it.hasNext();)
      links.add(this.getLinkFromID(it.next()));
    return links;
  }

  public BNode getNodeFromID(Integer id) {
    return nodes_.get(id);
  }

  public BNode opposite(BNode node, BLink link) {
    if (node.isAddressNode()) {
      return ((BLinkStreetPartial) link).getEndNode();
    }
    if (link.getClassType() == BLink.CLASS_STREETPARTIAL) {
      return ((BLinkStreetPartial) link).getAddressNode();
    }
    return graph_.opposite(node, link);
  }

  public Set<BLink> incidentLinks(BNode node) {
    Set<BLink> links = new HashSet<BLink>();
    links.addAll(graph_.edgesOf(node));
    return links; //graph_.edgesOf(node);
  }

  public boolean areIncident(BNode node, BLink link) {
    return graph_.edgesOf(node).contains(link);
  }

  public Set<BLink> connectingLinks(BNode node1, BNode node2) {
    return graph_.getAllEdges(node1, node2);
  }

  public BNode commonNode(BLink l1, BLink l2) {
    if (l1.getFNode() == l2.getFNode() || l1.getFNode() == l2.getTNode()) {
      return l1.getFNode();
    }
    if (l1.getTNode() == l2.getFNode() || l1.getTNode() == l2.getTNode()) {
      return l1.getTNode();
    }
    return null;
  }

  public boolean areAdjacent(BNode n1, BNode n2) {
    return !Collections.disjoint(graph_.edgesOf(n1), graph_.edgesOf(n2));
  }

  public int linkCount(BNode node) {
    return graph_.degreeOf(node);
  }

  public int walkableLinkCount(BNode node) {
    Iterator<BLink> links = incidentLinks(node).iterator();
    int count = 0;
    while (links.hasNext()) {
      if (links.next().isWalkable()) {
        count++;
      }
    }
    return count;
  }

  public Set<BLink> getLinks(Iterator<BNode> nodes, int typeCode) {
    Set<BLink> links = new HashSet<BLink>();

    switch (typeCode) {
      case 0:
        while (nodes.hasNext()) {
          BNode node = nodes.next();
          Iterator<BLink> iLinks = incidentLinks(node).iterator();
          while (iLinks.hasNext()) {
            BLink link = iLinks.next();
            if (!links.contains(link)) {
              links.add(link);
            }
          }
        }
        break;
      case 1:
        while (nodes.hasNext()) {
          BNode node = nodes.next();
          Iterator<BLink> iLinks = incidentLinks(node).iterator();
          while (iLinks.hasNext()) {
            BLink link = iLinks.next();
            int typeCat = link.getType() / 10;
            if (!links.contains(link) && (typeCat == LinkTypes.MINOR_TFARE || typeCat == LinkTypes.MAJOR_TFARE || typeCat == LinkTypes.TRANSIT)) {
              links.add(link);
            }
          }
        }
        break;
      case 2:
        while (nodes.hasNext()) {
          BNode node = nodes.next();
          Iterator<BLink> iLinks = incidentLinks(node).iterator();
          while (iLinks.hasNext()) {
            BLink link = iLinks.next();
            int typeCat = link.getType() / 10;
            if (!links.contains(link) && (typeCat == LinkTypes.MAJOR_TFARE || typeCat == LinkTypes.TRANSIT)) {
              links.add(link);
            }
          }
        }
        break;
    }
    return links;
  }

  public double getMinNodeXCoord() {
    double min = Double.MAX_VALUE;
    for(BNode node : getNodes()) min = Math.min(node.x_, min);
    return min;
  }

  public double getMaxNodeXCoord() {
    double max = Double.MIN_VALUE;
    for(BNode node : getNodes()) max = Math.max(node.x_, max);
    return max;
  }

  public double getMinNodeYCoord() {
    double min = Double.MAX_VALUE;
    for(BNode node : getNodes()) min = Math.min(node.y_, min);
    return min;
  }

  public double getMaxNodeYCoord() {
    double max = Double.MIN_VALUE;
    for(BNode node : getNodes()) max = Math.max(node.y_, max);
    return max;
  }

  // add node/link to network

  public void addNode(BNode node) {
    graph_.addVertex(node);
    nodes_.put(new Integer(node.getID()), node);
    if (node.getID() > maxNodeID_) {
      maxNodeID_ = node.getID();
    }
  }

  public void addLink(BLink link) {
    //if(link.getClassType()==CLink.CLASS_RAILROAD) System.out.println("added link to network, "+link.getFNodeID()+","+link.getTNodeID());
    graph_.addEdge(link.getFNode(), link.getTNode(), link);
    links_.put(new Integer(link.getID()), link);
    if (link.getID() > maxLinkID_) {
      maxLinkID_ = link.getID();
    }
  }

  public void deleteNode(BNode node) {
    graph_.removeVertex(node);
    nodes_.remove(node.getID());
  }

  public void deleteLink(BLink link) {
    graph_.removeEdge(link);
    links_.remove(link.getID());
  }

  public BLinkDirect createNewDLink(Scenario s, BNode fNode, BNode tNode, int dist) {
    int id = newDLinkID();
    //msg("  dlink id: "+id);
    BLinkDirect dlink = new BLinkDirect(s, id, fNode, tNode, "", dist);
    links_.put(new Integer(id), dlink);
    graph_.addEdge(fNode, tNode, dlink);
    return dlink;
  }

  // link modification

  public BLink changeLinkType(BLink link, int newType) {
    return changeLinkType(link, newType, false);
  }
  
  public BLink changeLinkType(BLink link, int newType, boolean preserveStreetDirection) {
    int newClassType = LinkTypes.linkTypetoClassType(newType);
    // class type does not change:
    if(link.getClassType() == newClassType) {
      if (link.getClassType() == BLink.CLASS_STREET && preserveStreetDirection)
        ((BLinkStreet) link).setTypePreserveDirection(newType);
      else link.setType(newType);
    }
    else { // class type does change
      switch(newClassType) {
        case BLink.CLASS_BIKEPED:
          link = new BLinkBikePed(link); break;
        case BLink.CLASS_METRO:
          link = new BLinkMetro(link); break;
        case BLink.CLASS_LRT:
          link = new BLinkLRT(link); break;
        case BLink.CLASS_RAILROAD:
          link = new BLinkRR(link); break;
        case BLink.CLASS_STREET:
          link = new BLinkStreet(link, newType); break;
      }
      links_.put(link.getID(), link);
    }
    return link;
  }

  public void setScenLinkOverride(BLink link, short scenID, int newType) {
    Map<Short, Integer> typeLookup = typeOverrides_.get(link.getID());
    if(typeLookup != null) {
      typeLookup.put(scenID, newType);
    }
    else {
      typeLookup = new HashMap<Short, Integer>();
      typeLookup.put(scenID, newType);
      typeOverrides_.put(link.getID(), typeLookup);
    }
    link.setTypeOverride(true);
  }

  public void clearScenLinkOverrides(BLink link) {
    typeOverrides_.remove(link.getID());
    link.setTypeOverride(false);
  }

  public Map<Short, Integer> getScenLinkOverrides(int linkID) {
    return typeOverrides_.get(linkID);
  }

  /*public int getOverriddenFullType(BLink link, short scenID) {
    Map<Short, Integer> typeLookup = typeOverrides_.get(link.getID());
    return (typeLookup.containsKey(scenID)) ? typeLookup.get(scenID) : link.getExtendedType();
  }*/

  public int getOverriddenFullType(BLink link, Scenario scen) {
    Map<Short, Integer> typeLookup = typeOverrides_.get(link.getID());

    while(scen != link.getScenario()) {
      if(typeLookup.containsKey(scen.getID())) return typeLookup.get(scen.getID());
      scen = scen.getParent();
    }

    return link.getExtendedType();
    //return (typeLookup.containsKey(scenID)) ? typeLookup.get(scenID) : link.getExtendedType();
  }

  public int getOverriddenType(BLink link, short scenID) {
    Map<Short, Integer> typeLookup = typeOverrides_.get(link.getID());
    return (typeLookup.containsKey(scenID)) ? typeLookup.get(scenID)/100 : link.getType();
  }

  /*public int getOverriddenTransitProv(BLinkStreet link, short scenID) {
    Map<Short, Integer> typeLookup = typeOverrides_.get(link.getID());
    return (typeLookup.containsKey(scenID)) ? typeLookup.get(scenID)/10 % 10 : link.getTransitProv();
  }*/

  public int getOverriddenTransitProv(BLinkStreet link, Scenario scen) {
    return getOverriddenFullType(link, scen)/10 % 10;
  }

  public int getOverriddenBikePedProv(BLinkStreet link, short scenID) {
    Map<Short, Integer> typeLookup = typeOverrides_.get(link.getID());
    return (typeLookup.containsKey(scenID)) ? typeLookup.get(scenID) % 10 : link.getBikePedProv();
  }

  // misc

  public Iterator<BLink> findLinksToNodeSet(BLink link, Set<BNode> destNodes) {
    List<BLink> fromList = new LinkedList<BLink>(), toList = new LinkedList<BLink>();
    //System.out.println("NA START");
    //System.out.println("NA - from");
    boolean fres = findLinksToNodeSetRecurse(0, link, link.getFNode(), fromList, destNodes);
    //System.out.println("NA - to");
    boolean tres = findLinksToNodeSetRecurse(0, link, link.getTNode(), toList, destNodes);
    if (!tres && !fres) {
      return null;
    }
    int fromSize = fres ? fromList.size() : 0;
    int toSize = tres ? toList.size() : 0;
    List<BLink> finalList = (fromSize > toSize) ? fromList : toList;
    finalList.add(link);
    return finalList.iterator();
  }

  private boolean findLinksToNodeSetRecurse(int iter, BLink link, BNode node, List<BLink> list, Set<BNode> destNodes) {
    iter++;
    if (iter > 16) {
      System.out.println("OVERFLOW");
      return false;
    }
    //if(node == endNode() || node == startNode()) return true;
    if (destNodes.contains(node)) {
      return true;
    }

    //EdgeIterator edges = engine_.getGraph().incidentEdges(node.getVertex());
    Iterator<BLink> iLinks = incidentLinks(node).iterator();
    String name = link.getDisplayName();
    //System.out.println("in addNAT("+iter+"): "+name);
    while (iLinks.hasNext()) {
      //Edge edge = edges.nextEdge();
      BLink iLink = iLinks.next(); //(CLink) edge.element();
      if (link != iLink) {
        String iName = iLink.getDisplayName();
        //System.out.println("  comp to "+iName);
        if (FPUtil.stripQuadrant(iName).compareTo(FPUtil.stripQuadrant(name)) == 0) {
          //System.out.println("  Success: "+iName+" and "+name);
          list.add(0, iLink);
          return findLinksToNodeSetRecurse(iter, iLink, opposite(node, iLink), list, destNodes);
        //break;
        }
      }
    }
    return false;
  }

  public ListenableDijkstra getDijkstra(BNode start, BNode end) {
    TraversalListener listener = new TraversalListenerAdapter<BNode, BLink>() {

      @Override
      public void vertexTraversed(VertexTraversalEvent<BNode> e) {
        //System.out.println("node "+e.getVertex().getID());
        graph_.setLastTraversed(e.getVertex());
      }
    };
    return new ListenableDijkstra(graph_, start, end, listener);
  }

  public void clearTransitAnnotations() {
    Set<Integer> toRemove = new HashSet<Integer>();

    Iterator<BLink> links = links_.values().iterator();
    while (links.hasNext()) {
      BLink link = links.next();
      link.unregisterAllSubRoutes();
      if (link.getID() < 0) {
        graph_.removeEdge(link);
        toRemove.add(link.getID());
      }
    }
    Iterator<Integer> remlinks = toRemove.iterator();
    while (remlinks.hasNext()) {
      links_.remove(remlinks.next());
    }
    minDLinkID_ = 0;
  }

  // file i/o -- legacy

  public void writeToFile(String filename) {
    FileOutputStream fos = null;
    try {
      fos = new FileOutputStream(filename);
      ObjectOutputStream oos = new ObjectOutputStream(fos);

      oos.writeInt(nodes_.size());
      Iterator<BNode> nodes = nodes_.values().iterator();
      while (nodes.hasNext()) {
        oos.writeObject(nodes.next());
      }

      System.out.println("wrote nodes");

      oos.writeInt(links_.size());
      Iterator<BLink> links = links_.values().iterator();
      while (links.hasNext()) {
        oos.writeObject(links.next());
      }


      //oos.writeObject(links_);
      System.out.println("wrote links");

      oos.close();
    } catch (Exception ex) {
      ex.printStackTrace();
    } finally {
      try {
        fos.close();
      } catch (IOException ex) {
        Logger.getLogger(Routes.class.getName()).log(Level.SEVERE, null, ex);
      }
    }
  }

  public void readFromFile(String filename, FPEngine engine) {
    System.out.println("Loading network data from FPN file");
    FileInputStream fis = null;
    try {
      fis = new FileInputStream(filename);
      ObjectInputStream ois = new ObjectInputStream(fis);

      int nodeCount = ois.readInt();
      System.out.println("nodeCount = " + nodeCount);
      for (int i = 0; i < nodeCount; i++) {
        BNode node = (BNode) ois.readObject();
        node.init(engine);
        addNode(node);
      }

      /*System.out.println("reading links..");
      links_ = (Map<Integer, CLink>) ois.readObject();
      System.out.println("..done reading links");*/
      int linkCount = ois.readInt();
      System.out.println("linkCount = " + linkCount);
      for (int i = 0; i < linkCount; i++) {
        BLink link = (BLink) ois.readObject();
        link.init(engine);
        addLink(link);
        if (i % 10000 == 0) {
          System.out.println("read " + i + " links");
        }
      }

      ois.close();
    } catch (Exception ex) {
      //Logger.getLogger(Routes.class.getName()).log(Level.SEVERE, null, ex);
      ex.printStackTrace();
    } finally {
      try {
        fis.close();
      } catch (IOException ex) {
        Logger.getLogger(Routes.class.getName()).log(Level.SEVERE, null, ex);
      }
    }
  }
}
